Skip to content

Pad validator withdrawals with a reward accrual buffer - #846

Merged
cyc60 merged 12 commits into
masterfrom
dynamic-withdrawal-buffer
Sep 22, 2026
Merged

cyc60 merged 12 commits into
masterfrom
dynamic-withdrawal-buffer

Conversation

@cyc60

@cyc60 cyc60 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Description

The operator has been sending withdrawValidators transactions for dust amounts (61 gwei, 62 gwei, 3954 gwei) roughly every 12 hours on the Serenita vault, each costing more in gas than it withdraws.

Root cause: the vault exit queue is share-denominated, so queued positions keep accruing rewards at every oracle update. The operator requested exactly the getExitQueueMissingAssets shortfall, floored to 1 gwei and truncated down. The requested withdrawal only reaches the vault after the consensus layer delay (about 27h for a partial withdrawal, up to ~11 days for a full validator exit) plus the next harvest, so every reward update during that window produced a fresh shortfall of ~12h of rewards on the remaining queue, and the operator immediately chased it with another transaction.

Changes:

  • get_queued_assets now returns ExitQueueAssets(missing, total). missing is the net shortfall as before. total is the value of the queued exit shares, the part of the queue that keeps accruing rewards. New VaultContract.get_queued_exit_assets reads getExitQueueData, totalAssets and totalShares in one multicall after updateState and converts the shares with OsTokenConverter. The checker cannot provide this value since it nets out the vault's liquid assets. Legacy asset-denominated exits are fixed and excluded. The extra read is skipped when there is no shortfall.
  • New calculate_withdrawal_buffer pads the request with max(total * WITHDRAWAL_BUFFER_BPS / 10000, MIN_WITHDRAWAL_BUFFER_GWEI). 10 bps (0.1%) of the queue covers about 18 days of rewards at 2% APR, enough for both the partial withdrawal and the full exit path without modelling consensus layer processing. The floor covers queues too small for the ratio. Excess lands as withdrawable assets and is re-staked by the normal funding path, so the buffer is capped at 1 ETH (MAX_WITHDRAWAL_BUFFER_GWEI): a 2600 ETH queue would otherwise ask for 2.6 ETH that only goes straight back to funding.
  • MIN_WITHDRAWAL_BUFFER_GWEI is an env setting (default 10000 gwei), checked at startup to be between 0 and the 1 ETH cap.
  • The buffer is applied only on the partial-withdrawal path; the partial-vs-full decision still uses the unbuffered shortfall, so the buffer can never force a full validator exit. In the full-exit path the partial top-up is skipped once a full exit covers the shortfall, so the buffer alone never produces a partial request.
  • MISSING_ASSETS_THRESHOLD_GWEI becomes an env setting (default 1 gwei, so any positive shortfall is served and a small exit is paid once rather than left in the queue). A startup check caps it at 0.01 ETH, mirroring the oracle's MISSING_ASSETS_THRESHOLD: above that the operator would skip shortfalls the oracle already treats as exit-worthy and covers with a full validator exit.

For a 120 ETH queue the buffer is 0.12 ETH; for a 164 gwei remainder it is the 10000 gwei floor; for a 2600 ETH queue it is the 1 ETH cap.

tsudmi
tsudmi previously approved these changes Sep 20, 2026
Comment thread src/withdrawals/assets.py Outdated
Comment thread src/config/settings.py
@cyc60
cyc60 merged commit a08262e into master Sep 22, 2026
10 checks passed
@cyc60
cyc60 deleted the dynamic-withdrawal-buffer branch September 22, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants